查看原文
其他

Arthas实践--抽丝剥茧排查线上应用日志打满问题

横云断岭 横云断岭的专栏 2019-06-27


现象

在应用的 service_stdout.log里一直输出下面的日志,直接把磁盘打满了:

  1. 23:07:34.441 [TAIRCLIENT-1-thread-1] DEBUG io.netty.channel.nio.NioEventLoop - Selector.select() returned prematurely 14 times in a row.

  2. 23:07:34.460 [TAIRCLIENT-1-thread-3] DEBUG io.netty.channel.nio.NioEventLoop - Selector.select() returned prematurely 3 times in a row.

  3. 23:07:34.461 [TAIRCLIENT-1-thread-4] DEBUG io.netty.channel.nio.NioEventLoop - Selector.select() returned prematurely 3 times in a row.

  4. 23:07:34.462 [TAIRCLIENT-1-thread-5] DEBUG io.netty.channel.nio.NioEventLoop - Selector.select() returned prematurely 3 times in a row.

service_stdout.log是进程标准输出的重定向,可以初步判定是tair插件把日志输出到了stdout里。

尽管有了初步的判断,但是具体logger为什么会打到stdout里,还需要进一步排查,常见的方法可能是本地debug。

下面介绍利用arthas直接在线上定位问题的过程,主要使用 scgetstatic命令。

  • https://alibaba.github.io/arthas/sc.html

  • https://alibaba.github.io/arthas/getstatic.html

定位logger的具体实现

日志是 io.netty.channel.nio.NioEventLoop输出的,到netty的代码里查看,发现是DEBUG级别的输出:

  • https://github.com/netty/netty/blob/netty-4.0.35.Final/transport/src/main/java/io/netty/channel/nio/NioEventLoop.java#L673

然后用arthas的 sc命令来查看具体的 io.netty.channel.nio.NioEventLoop是从哪里加载的。

  1. class-info        io.netty.channel.nio.NioEventLoop

  2. code-source       file:/opt/app/plugins/tair-plugin/lib/netty-all-4.0.35.Final.jar!/

  3. name              io.netty.channel.nio.NioEventLoop

  4. isInterface       false

  5. isAnnotation      false

  6. isEnum            false

  7. isAnonymousClass  false

  8. isArray           false

  9. isLocalClass      false

  10. isMemberClass     false

  11. isPrimitive       false

  12. isSynthetic       false

  13. simple-name       NioEventLoop

  14. modifier          final,public

  15. annotation

  16. interfaces

  17. super-class       +-io.netty.channel.SingleThreadEventLoop

  18.                     +-io.netty.util.concurrent.SingleThreadEventExecutor

  19.                       +-io.netty.util.concurrent.AbstractScheduledEventExecutor

  20.                         +-io.netty.util.concurrent.AbstractEventExecutor

  21.                           +-java.util.concurrent.AbstractExecutorService

  22.                             +-java.lang.Object

  23. class-loader      +-tair-plugin's ModuleClassLoader

  24. classLoaderHash   73ad2d6

可见,的确是从tair插件里加载的。

查看NioEventLoop的代码,可以发现它有一个 logger的field:

  1. public final class NioEventLoop extends SingleThreadEventLoop {

  2.    private static final InternalLogger logger = InternalLoggerFactory.getInstance(NioEventLoop.class);

使用arthas的 getstatic命令来查看这个 logger具体实现类是什么(使用 -c参数指定classloader):

  1. $ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'getClass().getName()'

  2. field: logger

  3. @String[io.netty.util.internal.logging.Slf4JLogger]

可以发现是 Slf4JLogger

再查看io.netty.util.internal.logging.Slf4JLogger的实现,发现它内部有一个logger的field:

  1. package io.netty.util.internal.logging;

  2. import org.slf4j.Logger;

  3. /**

  4. * <a href="http://www.slf4j.org/">SLF4J</a> logger.

  5. */

  6. class Slf4JLogger extends AbstractInternalLogger {

  7.    private static final long serialVersionUID = 108038972685130825L;

  8.    private final transient Logger logger;

那么使用arthas的 getstatic命令来查看这个 logger属性的值:

  1. $ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger'

  2. field: logger

  3. @Logger[

  4.    serialVersionUID=@Long[5454405123156820674],

  5.    FQCN=@String[ch.qos.logback.classic.Logger],

  6.    name=@String[io.netty.channel.nio.NioEventLoop],

  7.    level=null,

  8.    effectiveLevelInt=@Integer[10000],

  9.    parent=@Logger[Logger[io.netty.channel.nio]],

  10.    childrenList=null,

  11.    aai=null,

  12.    additive=@Boolean[true],

  13.    loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],

  14. ]

可见,logger的最本质实现类是: ch.qos.logback.classic.Logger

再次用 getstatic命令来确定jar包的location:

  1. $ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger.getClass().getProtectionDomain().getCodeSource().getLocation()'

  2. field: logger

  3. @URL[

  4.    BUILTIN_HANDLERS_PREFIX=@String[sun.net.www.protocol],

  5.    serialVersionUID=@Long[-7627629688361524110],

  6.    protocolPathProp=@String[java.protocol.handler.pkgs],

  7.    protocol=@String[jar],

  8.    host=@String[],

  9.    port=@Integer[-1],

  10.    file=@String[file:/opt/app/plugins/tair-plugin/lib/logback-classic-1.2.3.jar!/],

  11.    query=null,

  12.    authority=@String[],

  13.    path=@String[file:/opt/app/plugins/tair-plugin/lib/logback-classic-1.2.3.jar!/],

  14.    userInfo=null,

  15.    ref=null,

  16.    hostAddress=null,

  17.    handler=@Handler[com.taobao.pandora.loader.jar.Handler@1a0c361e],

  18.    hashCode=@Integer[126346621],

  19.    tempState=null,

  20.    factory=@TomcatURLStreamHandlerFactory[org.apache.catalina.webresources.TomcatURLStreamHandlerFactory@3edd7b7],

  21.    handlers=@Hashtable[isEmpty=false;size=4],

  22.    streamHandlerLock=@Object[java.lang.Object@488ccac9],

  23.    serialPersistentFields=@ObjectStreamField[][isEmpty=false;size=7],

  24. ]

可见这个 ch.qos.logback.classic.Logger的确是tair插件里加载的。

定位logger的level

上面已经定位logger的实现类是 ch.qos.logback.classic.Logger,但是为什么它会输出 DEBUG level的日志?

其实在上面的 getstatic-c73ad2d6io.netty.channel.nio.NioEventLooplogger'logger'输出里,已经打印出它的level是null了。如果对logger有所了解的话,可以知道当child logger的level为null时,它的level取决于parent logger的level。

我们再来看下 ch.qos.logback.classic.Logger的代码,它有一个parent logger的属性:

  1. public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {

  2.    /**

  3.     * The parent of this category. All categories have at least one ancestor

  4.     * which is the root category.

  5.     */

  6.    transient private Logger parent;

所以,我们可以通过 getstatic来获取到这个parent属性的内容。然后通过多个parent操作,可以发现level都是null,最终发现ROOT level是DEBUG 。

  1. $ getstatic -c 73ad2d6 io.netty.channel.nio.NioEventLoop logger 'logger.parent.parent.parent.parent.parent'

  2. field: logger

  3. @Logger[

  4.    serialVersionUID=@Long[5454405123156820674],

  5.    FQCN=@String[ch.qos.logback.classic.Logger],

  6.    name=@String[ROOT],

  7.    level=@Level[DEBUG],

  8.    effectiveLevelInt=@Integer[10000],

  9.    parent=null,

  10.    childrenList=@CopyOnWriteArrayList[isEmpty=false;size=2],

  11.    aai=@AppenderAttachableImpl[ch.qos.logback.core.spi.AppenderAttachableImpl@1ecf9bae],

  12.    additive=@Boolean[true],

  13.    loggerContext=@LoggerContext[ch.qos.logback.classic.LoggerContext[default]],

  14. ]

所以 io.netty.channel.nio.NioEventLoop的logger的level取的是ROOT logger的配置,即默认值 DEBUG

具体实现可以查看 ch.qos.logback.classic.LoggerContext

  1.    public LoggerContext() {

  2.        super();

  3.        this.loggerCache = new ConcurrentHashMap<String, Logger>();

  4.        this.loggerContextRemoteView = new LoggerContextVO(this);

  5.        this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);

  6.        this.root.setLevel(Level.DEBUG);

  7.        loggerCache.put(Logger.ROOT_LOGGER_NAME, root);

  8.        initEvaluatorMap();

  9.        size = 1;

  10.        this.frameworkPackages = new ArrayList<String>();

  11.    }

为什么logback输出到了stdout里

上面我们得到结论

  • tair插件里的logback默认的level是DEBUG,导致netty里的日志可以被打印出来

那么我们可以猜测:

  • tair里的logback没有特殊配置,或者只配置了tair自己的package,导致ROOT logger的日志直接输出到stdout里

那么实现上tair里是使用了 logger-api,它通过 LoggerFactory.getLogger函数获取到了自己package的logger,然后设置level为 INFO,并设置了appender。

换而言之,tair插件里的logback没有设置ROOT logger,所以它的默认level是DEBUG,并且默认的appender会输出到stdout里。

所以tair插件可以增加设置ROOT logger level为 INFO来修复这个问题。

  1. private static com.taobao.middleware.logger.Logger logger

  2.            = com.taobao.middleware.logger.LoggerFactory.getLogger("com.taobao.tair");

  3.    public static com.taobao.middleware.logger.Logger infolog

  4.            = com.taobao.middleware.logger.LoggerFactory.getLogger("com.taobao.tair.custom-infolog");

  5.    public static int JM_LOG_RETAIN_COUNT = 3;

  6.    public static String JM_LOG_FILE_SIZE = "200MB";

  7.    static {

  8.        try {

  9.            String tmp = System.getProperty("JM.LOG.RETAIN.COUNT", "3");

  10.            JM_LOG_RETAIN_COUNT = Integer.parseInt(tmp);

  11.        } catch (NumberFormatException e) {

  12.        }

  13.        JM_LOG_FILE_SIZE = System.getProperty("JM.LOG.FILE.SIZE", "200MB");

  14.        logger.setLevel(Level.INFO);

  15.        logger.activateAppenderWithSizeRolling("tair-client", "tair-client.log", "UTF-8",

  16.                TairUtil.splitSize(JM_LOG_FILE_SIZE, 0.8 / (JM_LOG_RETAIN_COUNT + 1)), JM_LOG_RETAIN_COUNT);

  17.        logger.setAdditivity(false);

  18.        logger.activateAsync(500, 100);

  19.        logger.info("JM_LOG_RETAIN_COUNT " + JM_LOG_RETAIN_COUNT + " JM_LOG_FILE_SIZE " + JM_LOG_FILE_SIZE);

  20.        infolog.setLevel(Level.INFO);

  21.        infolog.activateAppenderWithSizeRolling("tair-client", "tair-client-info.log", "UTF-8", "10MB", 1);

  22.        infolog.setAdditivity(false);

  23.        infolog.activateAsync(500, 100);

总结

  • tair插件里直接以api的方式设置了自己package下的logger

  • tair插件里netty的logger的packge和tair并不一样,所以它最终取的是ROOT logger的配置

  • logback默认的ROOT logger level是 DEBUG,输出是stdout

  • 利用arthas的 sc命令定位具体的类

  • 利用arthas的 getstatic获取static filed的值

  • 利用logger parent层联的特性,可以向上一层层获取到ROOT logger的配置

链接

  • Arthas开源:https://github.com/alibaba/arthas

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存